package cashpointer.core.actions;

import au.id.jericho.lib.html.Element;
import au.id.jericho.lib.html.Source;
import au.id.jericho.lib.html.Tag;
import cashpointer.core.dao.CashpointDao;
import cashpointer.core.dao.OwnerDao;
import cashpointer.core.model.Address;
import cashpointer.core.model.Cashpoint;
import cashpointer.core.model.GeoPoint;
import cashpointer.core.model.Owner;
import cashpointer.core.support.GeoPointFinder;
import cashpointer.core.support.PropertyStore;
import com.opensymphony.xwork2.ActionSupport;
import com.thoughtworks.xstream.XStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Akcja aktualizujaca baze bankomatow.
 * @author wojciech topolski
 * @version 2009-04-06
 */
public class UpdateAction extends ActionSupport {
    // Logger.
    static private Log log = LogFactory.getLog(UpdateAction.class);

    public String execute() {
        // Pobranie contextu Springa.
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

        // Pobranie contextStoreBean.
        CashpointDao cashpointDao = (CashpointDao) ctx.getBean("hibernateCashpointDao");

        // Pobranie propertyStoreBean.
        PropertyStore propertyStoreBean = (PropertyStore) ctx.getBean("propertyStoreBean");

        // Pobranie aktualnej wersji
        SimpleDateFormat dataFormat = new SimpleDateFormat("yyyyMMdd");
        String version = dataFormat.format(new Date());

        // Wykonanie aktualizacji bazy danych.
        try {
            makeUpdate(ctx, version);
        }
        catch (Exception e) {
            if (log.isErrorEnabled()) {
                log.error("[UpdateAction::execute()] ROLLBACK");
                log.error("[UpdateAction::execute()] Blad podczas aktualizacji bazy danych", e);
            }

            cashpointDao.rollback(version);
            return ERROR;
        }

        // Generowanie plikow XML
        System.out.println("Generowanie plikow XML...");

        // Zapisanie danych do plikow XML.
        List<Cashpoint> findedCashpoints = null;
        XStream xstream = new XStream();
		xstream.processAnnotations(Cashpoint.class);
		xstream.processAnnotations(Owner.class);
		xstream.setMode(XStream.NO_REFERENCES);

        // Katalog w ktorym sa skladowane pliki wyjsciowe.
        String outdataDir = propertyStoreBean.getPropertyByName("core.outdata");
        if (!(new File(outdataDir)).exists()) {
            (new File(outdataDir)).mkdir();
        }

        // Pobierz kolejno (pojedynczo) wszystkie bankomaty z aktualna wersja START.
        // Pobierz dane z bankomatow i zbuduj zawartosc pliku XML z bankomatami dodanymi w danej wersji.
        findedCashpoints = cashpointDao.getByStart(version);
        if (findedCashpoints != null) {
        	// Zmienne pomocnicze
        	int fileIndex = 1;
            int putRecordsCount = 0;
        	BufferedWriter file = null;
            
            try {
                Iterator it = findedCashpoints.iterator();
                while (it.hasNext()) {
                	// Nowy plik
                	if (putRecordsCount == 0) {
                		String filePostfix = String.format("%3d", fileIndex).replaceAll(" ", "0");
                		file = new BufferedWriter(new FileWriter(outdataDir + version + "." + filePostfix));
                    	file.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
                    	file.write("<list>");
                    	fileIndex++;
                	}
                	
                	// Dodanie bankomatu do pliku
                    file.write(xstream.toXML(it.next()));
                    putRecordsCount++;
                    
                    // Zamykam plik
                    if (putRecordsCount >= 250) {
                    	file.write("</list>");
                    	file.close();
                    	putRecordsCount = 0;
                    }
                }
                
                // Zamykam ostatni plik (gdy wartosc putRecordsCount nie zdarzyla osiagnac 250)
                if (putRecordsCount > 0 && putRecordsCount < 250) {
                	file.write("</list>");
                	file.close();
                	putRecordsCount = 0;
                }
                
                // Jedyny pusty plik, jesli nie ma zadnych zmian
                if (findedCashpoints.size() == 0) {
            		String filePostfix = String.format("%3d", fileIndex).replaceAll(" ", "0");
                	file = new BufferedWriter(new FileWriter(outdataDir + version + "." + filePostfix));
                	file.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
                	file.write("<list>");
                	file.write("</list>");
                	file.close();
                }
            }
            catch (IOException ioe) {
                if (log.isErrorEnabled()) {
                    log.error("[UpdateAction::execute()] Blad podczas generowania pliku add!!!", ioe);
                }
            }
        }

        // Pobierz kolejno (pojedynczo) wszystkie bankomaty z aktualna wersja STOP.
        // Pobierz dane z bankomatow i zbuduj zawartosc pliku XML z bankomatami dodanymi w danej wersji.
        findedCashpoints = cashpointDao.getByStop(version);
        if (findedCashpoints != null) {
            BufferedWriter file = null;
            try {
                file = new BufferedWriter(new FileWriter(outdataDir + version + ".000"));
                file.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
                file.write("<list>");
                for (Cashpoint c : findedCashpoints) {
                    file.write("<hashcode>");
                    file.write(String.valueOf(c.getHashcode()));
                    file.write("</hashcode>");
                }
                file.write("</list>");
                file.close();
            }
            catch (IOException ioe) {
                if (log.isErrorEnabled()) {
                    log.error("[UpdateAction::execute()] Blad podczas generowania pliku remove!!!", ioe);
                }
            }
        }

        // Pobierz kolejno (pojedynczo) wszystkie bankomaty z pusta wersja STOP i niepusta wrsja START.
        // Pobierz dane z bankomatow i zbuduj zawartosc pliku XML z bankomatami dodanymi w danej wersji.
        findedCashpoints = cashpointDao.getAllActive();
        if (findedCashpoints != null) {
        	// Zmienne pomocnicze
        	int fileIndex = 1;
            int putRecordsCount = 0;
        	BufferedWriter file = null;
            
            try {
                Iterator it = findedCashpoints.iterator();
                while (it.hasNext()) {
                	// Nowy plik
                	if (putRecordsCount == 0) {
                		String filePostfix = String.format("%3d", fileIndex).replaceAll(" ", "0");
                		file = new BufferedWriter(new FileWriter(outdataDir + "all." + version + "." + filePostfix));
                    	file.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
                    	file.write("<list>");
                    	fileIndex++;
                	}
                	
                	// Dodanie bankomatu do pliku
                    file.write(xstream.toXML(it.next()));
                    putRecordsCount++;
                    
                    // Zamykam plik
                    if (putRecordsCount >= 250) {
                    	file.write("</list>");
                    	file.close();
                    	putRecordsCount = 0;
                    }
                }
                
                // Zamykam ostatni plik (gdy wartosc putRecordsCount nie zdarzyla osiagnac 250)
                if (putRecordsCount > 0 && putRecordsCount < 250) {
                	file.write("</list>");
                	file.close();
                	putRecordsCount = 0;
                }

                // Jedyny pusty plik, jesli nie ma zadnych zmian
                if (findedCashpoints.size() == 0) {
            		String filePostfix = String.format("%3d", fileIndex).replaceAll(" ", "0");
            		file = new BufferedWriter(new FileWriter(outdataDir + "all." + version + "." + filePostfix));
                	file.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
                	file.write("<list>");
                	file.write("</list>");
                	file.close();
                }
            }
            catch (IOException ioe) {
                if (log.isErrorEnabled()) {
                    log.error("[UpdateAction::execute()] Blad podczas generowania pliku all!!!", ioe);
                }
            }
        }

        return SUCCESS;
    }

    private void makeUpdate(ApplicationContext ctx, String version) throws Exception {

        // Pobranie contextStoreBean.
        OwnerDao ownerDao = (OwnerDao) ctx.getBean("hibernateOwnerDao");

        // Pobranie coordinatesFinderBean.
        GeoPointFinder geoPointFinderBean = (GeoPointFinder) ctx.getBean("geoPointFinderBean");

        // Pobranie contextStoreBean.
        CashpointDao cashpointDao = (CashpointDao) ctx.getBean("hibernateCashpointDao");

        // Pobranie listy wlascicieli bankomatow.
        Set<Owner> owners = (Set<Owner>) ownerDao.getAll();

        // KOROK 1
        System.out.println("Zaczytywanie tablicy bankomatow z bazy danych...");

        // Pobierz hashCode wszystkich bankomatow zapisanych w bazie danych.
        Map<Integer, Integer> cashpointsHashcodes = cashpointDao.getAllHashCodes();
        if (log.isDebugEnabled()) {
            log.debug("[UpdateAction::execute()] Ilosc hashCode zaczytanych: " + cashpointsHashcodes.size());
        }

        // KOROK 2
        System.out.println("Pobieranie bankomatow z intenetu...");

        // Zmienne pomocnicze
        Boolean nextPage = true;
        int pageNumber = 0;
        Set<Integer> newCashpointsHashcodes = new HashSet<Integer>();

        // Czytanie strony...
        while (nextPage) {
            // Preparowanie adresu strony np. http://www.money.pl/banki/bankomaty/,-p0.html
            StringBuilder page = new StringBuilder("http://www.money.pl/banki/bankomaty/,-p");
            page.append(pageNumber);
            page.append(".html");

            // Wykonanie zapytania i pobranie odpowiedzi.
            Source source = null;
            try {
                source = new Source(new URL(page.toString()));
                if (source == null) {
                    throw new Exception("[UpdateAction::execute] source jest null");
                }
            } catch (IOException ioe) {
                throw new Exception("[UpdateAction::execute] Blad", ioe);
            }

            // Przeczytaj dane bankomatow znajdujacych sie na stronie i zbuduj kontener zawierajacy liste bankomatow.
            List<Cashpoint> newCashpoints = new ArrayList<Cashpoint>();
            Cashpoint newCashpoint = new Cashpoint();
            Address newAddress = new Address();

            // Zmienne pomocnicze
            Boolean isRecording = false;
            int indexTd = 0;

            // Sprawdzam czy koniec bankomatow
            List segments = source.findAllElements(Tag.DIV);
            for (Iterator i = segments.iterator(); i.hasNext();) {
            	Element element = (Element) i.next();
                String elementValue = element.getContent().toString();
                if (elementValue.startsWith("Nie znaleziono bankomat")) {
                	nextPage = false;
                	break;
                }
            }
            
            // Pobranie wszystkich wystapiec bloku TD na stronie w kontener listy.
            segments = source.findAllElements(Tag.TD);
            for (Iterator i = segments.iterator(); i.hasNext();) {
                // Pobranie wezla z zawartoscia
                Element element = (Element) i.next();
                String elementValue = element.getContent().toString();

                // Szukanie tabelki z bankomatami.
                if (isRecording == false) {
                    if ("Operator".equalsIgnoreCase(elementValue)) {
                        element = (Element) i.next();
                        elementValue = element.getContent().toString();
                        if ("Województwo".equalsIgnoreCase(elementValue)) {
                            element = (Element) i.next();
                            elementValue = element.getContent().toString();
                            if ("Miasto".equalsIgnoreCase(elementValue)) {
                                element = (Element) i.next();
                                elementValue = element.getContent().toString();
                                if ("Adres".equalsIgnoreCase(elementValue)) {
                                    element = (Element) i.next();
                                    elementValue = element.getContent().toString();
                                    if ("Czynny".equalsIgnoreCase(elementValue)) {
                                        isRecording = true;
                                    }
                                }
                            }
                        }
                    }
                }
                // Pobieranie zawartosci tabelki z bankomatami.
                else {
                    // Pobranie nazwy wlasciciela.
                    if (indexTd % 5 == 0) {
                        indexTd = 0;

                        elementValue = element.getContent().toString();
                        List aSubelements = element.findAllElements(Tag.A);
                        List spanSubelements = element.findAllElements(Tag.SPAN);

                        // Koniec bankomatow na stronie 0.
                        if ("".equalsIgnoreCase(elementValue.trim())) {
                            // Koniec czytania obecnej strony.
                            pageNumber++;
                            break;
                        }
                        // Nazwa kolejnego wlasciciela bez linku.
                        else if (spanSubelements.size() == 1) {
                            element = (Element) spanSubelements.get(0);
                            elementValue = element.getContent().toString();

                            // Uzupelnienie bankomatu o adres i dodanie do kontenera.
                            if (newCashpoint.getOwner() != null) {
                                newCashpoint.setAddress(newAddress);
                                newCashpoints.add(newCashpoint);
                            }

                            // Nowe obiekty dla nowego bankomatu.
                            newCashpoint = new Cashpoint();
                            newAddress = new Address();

                            // Pobierz wlasciciela dla kazdego z bankomatow na liscie Y uzywajac metody Owner owner = getOwner("wlasciciel");
                            newCashpoint.setOwner(getOwner(owners, elementValue, ownerDao));
                        }
                        // Nazwa kolejnego wlasciciela lub "wstecz".
                        else if (aSubelements.size() == 1) {
                            element = (Element) aSubelements.get(0);
                            elementValue = element.getContent().toString();

                            // "wstecz" koniec bankomatow na stronie.
                            if ("wstecz".equalsIgnoreCase(elementValue)) {
                                try {
                                    element = (Element) i.next(); // pomijam
                                    element = (Element) i.next(); // "dalej"
                                    elementValue = element.getContent().toString();
                                    if (elementValue != null && elementValue.contains("dalej")) {
                                        // Czytaj nastepna strone.
                                        nextPage = true;
                                    }
                                    else {
                                        // Ostatnia strona.
                                        nextPage = false;
                                    }
                                }
                                catch (Exception e) {
                                    // Ostatnia strona.
                                    nextPage = false;
                                    if (log.isErrorEnabled()) {
                                        log.error("[UpdateAction::execute] Blad!!!", e);
                                    }
                                }

                                // Koniec czytania obecnej strony.
                                pageNumber++;
                                break;
                            }
                            // Nazwa kolejnego wlasciciela.
                            else {
                                // Uzupelnienie bankomatu o adres i dodanie do kontenera.
                                if (newCashpoint.getOwner() != null) {
                                    newCashpoint.setAddress(newAddress);
                                    newCashpoints.add(newCashpoint);
                                }

                                // Nowe obiekty dla nowego bankomatu.
                                newCashpoint = new Cashpoint();
                                newAddress = new Address();

                                // Pobierz wlasciciela dla kazdego z bankomatow na liscie Y uzywajac metody Owner owner = getOwner("wlasciciel");
                                newCashpoint.setOwner(getOwner(owners, elementValue, ownerDao));
                            }
                        }
                        // Koniec czytania obecnej strony.
                        else {
                        	// Czasem mam taki babol: <div class="24633b29b6dcd467a729a48ad60885508955423198d049d1b68627f88fc0179e"></div>
                        	if (elementValue.startsWith("<div class=") && elementValue.endsWith("</div>")) {
                        		continue; // przechodze do nastepnego TD
                        	} else {
                        		if (log.isErrorEnabled()) {
                                	log.error("[UpdateAction::execute] Nastapil wyjatek w zakonczeniu listy bankomatow. Przebadac: " + elementValue);
                            	}
                            	pageNumber++;
                            	break;
                        	}
                        }
                    }

                    // Pobranie województwa.
                    if (indexTd % 5 == 1) {
                        newAddress.setRegion(elementValue);
                    }

                    // Pobranie miasta.
                    if (indexTd % 5 == 2) {
                        newAddress.setCity(elementValue);
                    }

                    // Pobranie adresu.
                    if (indexTd % 5 == 3) {
                        String[] elementValueArray = elementValue.split(" - ");
                        if (elementValueArray.length >= 2) {
                            newAddress.setStreet(elementValueArray[0]);
                            
                            StringBuilder info = new StringBuilder("");
                            for (int arrayId = 1; arrayId < elementValueArray.length; arrayId++) {
                                info.append(elementValueArray[arrayId]);
                            }
                            newAddress.setInfo(info.toString());
                        }
                        else {
                            newAddress.setStreet(elementValue);
                            newAddress.setInfo("");
                        }
                    }

                    // Pobranie czynnosci.
                    if (indexTd % 5 == 4) {
                        newCashpoint.setTime(elementValue);
                    }

                    indexTd++;
                }
            }

            for (Cashpoint c : newCashpoints) {
                // Obliczam hashCode dla bankomatu.
                c.setHashcode(c.hashCode());

                // Jesli dany bankomat juz znajduje sie w bazie danych.
                if (cashpointsHashcodes.keySet().contains(c.getHashcode())) {
                    cashpointsHashcodes.put(c.getHashcode(), 1); // 1 - ciagle istnieje

                    // Aktualizuje bankomat. Skoro ciagle istnieje na stronie to jego wersja STOP
                    // powinna byc NULL lub string pusty. Istnieje prawdopodobienstwo, ze raz dodany,
                    // zostal usuniety a nastepnie przywrocony.
                    Cashpoint cashpoint = cashpointDao.getByHashCode(c.getHashcode());
                    if (cashpoint != null && cashpoint.getStop() != null && cashpoint.getStop().length() != 0) {
                        if (log.isDebugEnabled()) {
                            log.debug("Bankomat zostaje przywrocony. Hashcode: " + cashpoint.getHashcode());
                        }
                        cashpoint.setStop(new String(""));
                        cashpoint.setStart(version);
                        cashpointDao.save(cashpoint);
                    }

                    if (log.isDebugEnabled()) {
                        log.debug("[UpdateAction::execute()] HashCode juz istnieje w bazie: " + c.getHashcode());
                    }
                }
                // Jesli zaczytano bankomat, ktory jest zduplikowany.
                else if (newCashpointsHashcodes.contains(c.getHashcode())) {
                    if (log.isDebugEnabled()) {
                        log.debug("[UpdateAction::execute()] HashCode zduplikowany na stronie: " + c.getHashcode());
                    }
                }
                // Dodaje nowy bankomat z odpowiednia wersja start postaci YYYYMMdd.
                else {
                    if (c.getAddress() != null) {
                        // Pobierz dla z bankomatow jego polozenie geograficzne.
                        // Jesli nie udalo sie okreslic polozenia nie zapisuj bankomatu.
                        GeoPoint gp = geoPointFinderBean.findByAddress(c.getAddress(), ctx);
                        if (gp != null && gp.getLatitude() != null && gp.getLongitude() != null) {
                            c.getAddress().setGeoPoint(gp);
                            c.setStart(version);
                            c.setStop(new String(""));
                            cashpointDao.save(c);
                            newCashpointsHashcodes.add(c.getHashcode());

                            if (log.isDebugEnabled()) {
                                log.debug("[UpdateAction::execute()] HashCode dodany: " + c.getHashcode());
                            }
                        }
                    }
                }
            }

            if (log.isDebugEnabled()) {
                log.debug("[UpdateAction::execute()] Zaczytano " + newCashpoints.size() + " bankomatów ze strony " + pageNumber);
            }

            // KOROK 3
            System.out.println("Przeczytano strone nr: " + pageNumber);
        }

        // KOROK 4
        System.out.println("Wyszukiwanie nieaktywnych bankomatow...");

        // Pobranie wszystkich numerow hashCode dla ktorych wartosc rozna sie 0
        // takie bankomaty nie zostaly odnalezione w updacie co sugeruje ze
        // sa nieaktywne. Nalezy je oznaczyc odpowiednia wesja.
        for (Integer hashcode : cashpointsHashcodes.keySet()) {
            Integer value = cashpointsHashcodes.get(hashcode);
            if (value == 0) {
                Cashpoint cashpoint = cashpointDao.getByHashCode(hashcode);
                if (cashpoint != null) {
                    if (cashpoint.getStop() == null || cashpoint.getStop().length() == 0) {
                        if (log.isDebugEnabled()) {
                            log.debug("[UpdateAction::execute()] Nieaktywny hashCode: " + hashcode);
                        }

                        cashpoint.setStop(version);
                        cashpointDao.save(cashpoint);
                    }
                }
                else {
                    if (log.isErrorEnabled()) {
                        log.error("[UpdateAction::execute()] Nie wybrano obiektu dla hashcode: " + hashcode);
                    }
                }
            }
        }
    }

    /**
     * Pobiera z wlasciciela o podanej nazwie. Jesli taki wlasciciel nie istnieje
     * funkcja go tworzy i dodaje do kontera i bazy.
     * @param name String
     * @return Owner
     */
    private Owner getOwner(Set<Owner> owners, String name, OwnerDao ownerDao) throws Exception {
        if (owners == null) {
            if (log.isDebugEnabled()) {
                log.debug("[UpdateAction::getOwner(String)] Kontener owners jest null");
            }

            throw new Exception("Kontener owners jest null");
        }

        if (name == null) {
            if (log.isDebugEnabled()) {
                log.debug("[UpdateAction::getOwner(String)] Zmienna name jest null");
            }

            return null;
        }

        Owner owner = new Owner();
        owner.setName(name);

        if (owners.contains(owner)) {
            Boolean finded = false;
            for (Owner o : owners) {
                if (o != null && name.equalsIgnoreCase(o.getName())) {
                    owner = o;
                    finded = true;
                    break;
                }
            }

            if (!finded) {
                return null;
            }
        }
        else {
            if (log.isDebugEnabled()) {
                log.debug("[UpdateAction::getOwner(String)] Przed zapisem OWNER ------");
            }

            ownerDao.save(owner);
            owners.add(owner);

            if (log.isDebugEnabled()) {
                log.debug("[UpdateAction::getOwner(String)] Po zapisie OWNER ------");
            }
        }

        return owner;
    }
}
